<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Bin文件合并工具 (Bin Merger/Padder)</title>
    <!-- 引入 Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        /* 使用 Inter 字体 */
        body {
            font-family: 'Inter', sans-serif;
            background-color: #f7f7f7;
            display: flex;
            justify-content: center;
            align-items: flex-start;
            min-height: 100vh;
            padding: 2rem;
        }
        .container {
            max-width: 800px;
            width: 95%;
            background-color: white;
            padding: 2rem;
            border-radius: 1rem;
            box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
        }
        .input-group label {
            font-weight: 600;
            color: #374151;
            margin-bottom: 0.5rem;
        }
        .input-text {
            border: 1px solid #d1d5db;
            padding: 0.5rem 0.75rem;
            border-radius: 0.5rem;
            transition: border-color 0.2s;
        }
        .input-text:focus {
            border-color: #4f46e5;
            outline: none;
            box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.2);
        }
    </style>
</head>
<body>

<div class="container">
    <h1 class="text-3xl font-bold text-center text-gray-800 mb-6 border-b pb-4">Bin文件合并工具</h1>
    
    <!-- 文件输入区 -->
    <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
        <!-- Bootloader 文件 -->
        <div class="input-group">
            <label for="bootFile" class="block text-sm">选择 boot.bin (可选)</label>
            <input type="file" id="bootFile" accept=".bin" class="w-full mt-1 p-2 border border-gray-300 rounded-lg">
        </div>
        <!-- App 文件 -->
        <div class="input-group">
            <label for="appFile" class="block text-sm">选择 app.bin (必选)</label>
            <input type="file" id="appFile" accept=".bin" class="w-full mt-1 p-2 border border-gray-300 rounded-lg">
        </div>
    </div>

    <!-- 参数配置区 -->
    <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
        <!-- 目标大小 -->
        <div class="input-group">
            <label for="targetSize" class="block text-sm">目标大小 (KB)</label>
            <input type="number" id="targetSize" value="512" min="1" class="input-text w-full mt-1" placeholder="例如: 512">
        </div>
        <!-- APP 偏移地址 -->
        <div class="input-group">
            <label for="appOffset" class="block text-sm">APP 偏移地址 (KB)</label>
            <input type="number" id="appOffset" value="12" min="0" class="input-text w-full mt-1" placeholder="例如: 12">
            <p class="text-xs text-gray-500 mt-1">仅当选择 boot.bin 时需要</p>
        </div>
        <!-- APP 偏移地址（字节）显示 -->
        <div class="input-group">
            <label class="block text-sm">APP 偏移地址 (字节)</label>
            <p id="appOffsetBytes" class="text-xl font-mono text-indigo-600 mt-1.5 p-1.5 bg-gray-50 rounded-lg border border-gray-200">0 Bytes</p>
        </div>
    </div>

    <!-- 操作按钮 -->
    <button id="processButton" class="w-full py-3 bg-indigo-600 text-white font-semibold rounded-lg hover:bg-indigo-700 transition duration-150 shadow-lg shadow-indigo-500/50">
        开始处理并生成 ARM32.bin (自动触发下载)
    </button>

    <!-- 下载区域 -->
    <div id="downloadArea" class="mt-6 hidden text-center">
        <p class="text-green-600 font-medium mb-3">文件处理完成！如果浏览器未自动弹出保存，请点击下方链接。</p>
        <a id="downloadLink" href="#" download="ARM32.bin" class="inline-block px-6 py-2 bg-green-500 text-white font-semibold rounded-lg hover:bg-green-600 transition duration-150">
            下载 ARM32.bin (备用链接)
        </a>
    </div>

    <!-- 日志输出区 -->
    <div class="mt-8 border-t pt-4">
        <h2 class="text-xl font-semibold mb-2 text-gray-700">处理日志</h2>
        <div id="logOutput" class="bg-gray-800 text-green-300 font-mono text-sm p-4 h-48 overflow-y-auto rounded-lg">
            等待输入文件和参数...
        </div>
    </div>
</div>

<script>
    // Constants
    const KB = 1024;
    const FF_BYTE = 0xFF; // 填充字节 0xFF

    // UI Elements
    const bootFileEl = document.getElementById('bootFile');
    const appFileEl = document.getElementById('appFile');
    const targetSizeEl = document.getElementById('targetSize');
    const appOffsetEl = document.getElementById('appOffset');
    const appOffsetBytesEl = document.getElementById('appOffsetBytes');
    const processButtonEl = document.getElementById('processButton');
    const logOutputEl = document.getElementById('logOutput');
    const downloadAreaEl = document.getElementById('downloadArea');
    const downloadLinkEl = document.getElementById('downloadLink');

    let bootData = null; // 存储 boot.bin 的 ArrayBuffer
    let appData = null;  // 存储 app.bin 的 ArrayBuffer

    // 浏览器安全限制说明
	log("作者：PN学堂甄家骏。", false);
    log("请选择文件并点击按钮开始处理。", false);

    /**
     * 将消息打印到日志区域
     * @param {string} message - 要打印的消息
     * @param {boolean} clear - 是否清空现有日志
     */
    function log(message, clear = false) {
        const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
        const entry = document.createElement('div');
        entry.textContent = `[${timestamp}] ${message}`;
        if (clear) {
            logOutputEl.innerHTML = '';
        }
        logOutputEl.appendChild(entry);
        logOutputEl.scrollTop = logOutputEl.scrollHeight; // 滚动到底部
    }

    /**
     * 将文件读取为 ArrayBuffer
     * @param {File} file - 文件对象
     * @returns {Promise<ArrayBuffer>}
     */
    function readFileAsArrayBuffer(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = (e) => resolve(e.target.result);
            reader.onerror = (e) => reject(new Error(`读取文件失败: ${e.target.error}`));
            reader.readAsArrayBuffer(file);
        });
    }

    /**
     * 更新 APP 偏移地址的字节显示
     */
    function updateAppOffsetDisplay() {
        const offsetKB = parseInt(appOffsetEl.value) || 0;
        const offsetBytes = offsetKB * KB;
        appOffsetBytesEl.textContent = `${offsetBytes.toLocaleString()} Bytes (0x${offsetBytes.toString(16).toUpperCase()})`;
    }

    // --- 事件监听 ---

    appOffsetEl.addEventListener('input', updateAppOffsetDisplay);
    window.onload = updateAppOffsetDisplay; // 初始加载时显示默认值

    bootFileEl.addEventListener('change', async (e) => {
        const file = e.target.files[0];
        if (file) {
            try {
                bootData = await readFileAsArrayBuffer(file);
                log(`已加载 boot.bin: ${file.name} (${(bootData.byteLength / KB).toFixed(2)} KB)`);
            } catch (error) {
                log(error.message);
            }
        } else {
            bootData = null;
            log("已清空 boot.bin 选择");
        }
    });

    appFileEl.addEventListener('change', async (e) => {
        const file = e.target.files[0];
        if (file) {
            try {
                appData = await readFileAsArrayBuffer(file);
                log(`已加载 app.bin: ${file.name} (${(appData.byteLength / KB).toFixed(2)} KB)`);
            } catch (error) {
                log(error.message);
            }
        } else {
            appData = null;
            log("请选择 app.bin");
        }
    });

    processButtonEl.addEventListener('click', processFiles);

    /**
     * 核心处理逻辑：拼接和填充文件
     */
    async function processFiles() {
        log("--- 开始处理文件 ---", true);
        downloadAreaEl.classList.add('hidden');
        
        if (!appData) {
            log("错误：请选择 app.bin 文件。", false);
            return;
        }

        // 1. 获取和校验参数
        const targetSizeKB = parseInt(targetSizeEl.value);
        const appOffsetKB = parseInt(appOffsetEl.value);

        if (isNaN(targetSizeKB) || targetSizeKB <= 0) {
            log("错误：目标大小必须是大于 0 的数字。", false);
            return;
        }
        if (isNaN(appOffsetKB) || appOffsetKB < 0) {
            log("错误：APP 偏移地址必须是非负数字。", false);
            return;
        }
        
        const targetSizeBytes = targetSizeKB * KB;
        const appOffsetBytes = appOffsetKB * KB;

        let finalBinArray = null; // 存储最终的 Uint8Array

        // 2. 判断处理模式：拼接 (Boot + App) 或 填充 (App Only)
        
        if (bootData) {
            // --- 模式 A: 拼接 (Boot + App) ---
            log("检测到 boot.bin，进入拼接模式。", false);

            const bootBytes = new Uint8Array(bootData);
            const appBytes = new Uint8Array(appData);
            
            const bootSize = bootBytes.byteLength;
            const appSize = appBytes.byteLength;

            log(`Boot 大小: ${bootSize / KB} KB`, false);
            log(`APP 大小: ${appSize / KB} KB`, false);
            log(`APP 期望偏移地址: ${appOffsetBytes / KB} KB (0x${appOffsetBytes.toString(16).toUpperCase()})`, false);

            if (appOffsetBytes < bootSize) {
                 log("错误：APP 偏移地址必须大于或等于 Boot 文件大小，否则 APP 将覆盖 Boot。", false);
                 return;
            }

            // 最小所需空间：bootSize + 间隙 + appSize
            const requiredSize = appOffsetBytes + appSize;
            
            if (requiredSize > targetSizeBytes) {
                log(`错误：拼接所需最小空间 (${requiredSize / KB} KB) 超过目标大小 (${targetSizeBytes / KB} KB)。`, false);
                return;
            }

            // 确定最终数组大小 (以所需的最小空间或目标大小的最大值)
            const arraySize = Math.max(requiredSize, targetSizeBytes);
            finalBinArray = new Uint8Array(arraySize);
            
            // 默认全部填充 0xFF
            finalBinArray.fill(FF_BYTE);

            // 1. 写入 Boot 数据 (从 0 开始)
            finalBinArray.set(bootBytes, 0);
            log(`成功写入 Boot (0KB - ${bootSize / KB} KB)。`, false);

            // 2. 写入 App 数据 (从 appOffsetBytes 开始)
            finalBinArray.set(appBytes, appOffsetBytes);
            log(`成功写入 App (从 ${appOffsetBytes / KB} KB 开始)。`, false);

            // 3. 间隙自动被 0xFF 填充 (因为我们在开始时 fill(FF_BYTE))
            const fillLength = appOffsetBytes - bootSize;
            log(`Boot 和 App 之间自动填充 0xFF 间隙：${fillLength} 字节。`, false);
            
            log(`拼接后的文件最小大小为 ${requiredSize / KB} KB。`, false);
            

        } else {
            // --- 模式 B: 填充 (App Only) ---
            log("未检测到 boot.bin，进入 App 单文件填充模式。", false);
            
            const appBytes = new Uint8Array(appData);
            const currentSize = appBytes.byteLength;
            
            log(`当前文件大小: ${currentSize / KB} KB`, false);
            log(`目标文件大小: ${targetSizeBytes / KB} KB`, false);

            if (currentSize > targetSizeBytes) {
                log(`提示：当前文件 (${currentSize / KB} KB) 已大于目标大小 (${targetSizeBytes / KB} KB)，无需填充。`, false);
                // 仅返回 App 数据
                finalBinArray = appBytes;
            } else {
                const padLength = targetSizeBytes - currentSize;
                log(`需要填充 ${padLength / KB} KB (共 ${padLength} 字节) 的 0xFF。`, false);
                
                // 创建新数组，大小为目标大小
                finalBinArray = new Uint8Array(targetSizeBytes);
                finalBinArray.fill(FF_BYTE); // 默认全部填充 0xFF
                finalBinArray.set(appBytes, 0); // 写入 App 数据
            }
        }
        
        // 3. 生成下载文件
        if (finalBinArray) {
            const finalSize = finalBinArray.byteLength;
            log(`最终 ARM32.bin 大小: ${finalSize / KB} KB。`, false);

            const blob = new Blob([finalBinArray.buffer], { type: 'application/octet-stream' });
            const url = URL.createObjectURL(blob);
            
            downloadLinkEl.href = url;
            downloadAreaEl.classList.remove('hidden');
            
            // **优化点：自动点击下载链接**
            downloadLinkEl.click();
            log("处理成功！已自动触发 ARM32.bin 下载。", false);
        } else {
            log("发生未知错误，未能生成文件。", false);
        }
    }
</script>

</body>
</html>